Package com.bric.swing

Source Code of com.bric.swing.DialogFooter$ClickAction

/*
* @(#)DialogFooter.java
*
* $Date: 2011-06-15 00:44:25 -0500 (Wed, 15 Jun 2011) $
*
* Copyright (c) 2011 by Jeremy Wood.
* All rights reserved.
*
* The copyright of this software is owned by Jeremy Wood.
* You may not use, copy or modify this software, except in 
* accordance with the license agreement you entered into with 
* Jeremy Wood. For details see accompanying license terms.
*
* This software is probably, but not necessarily, discussed here:
* http://javagraphics.java.net/
*
* That site should also contain the most recent official version
* of this software.  (See the SVN repository for more details.)
*/
package com.bric.swing;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.lang.reflect.Method;
import java.util.ResourceBundle;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JInternalFrame;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;

import com.bric.plaf.FocusArrowListener;
import com.bric.util.JVM;

/** This is a row of buttons, intended to be displayed at the
* bottom of a dialog.  This class is strongly related to the
* {@link com.bric.swing.QDialog} project, although the
* <code>DialogFooter</code> can exist by itself.
* <P>On the left of a footer are controls that should apply to the dialog itself,
* such as "Help" button, or a "Reset Preferences" button.
* On the far right are buttons that should dismiss this dialog.  They
* may be presented in different orders on different platforms based
* on the <code>reverseButtonOrder</code> boolean.
* <P>Buttons are also generally normalized, so the widths of buttons
* are equal.
* <P>This object will "latch onto" the RootPane that contains it.  It is assumed
* two DialogFooters will not be contained in the same RootPane. It is also
* assumed the same DialogFooter will not be passed around to several
* different RootPanes.
* <h3>Preset Options</h3>
* This class has several OPTION constants to create specific buttons.
* <P>In each constant the first option is the default button unless
* you specify otherwise.  The Apple Interface Guidelines advises:
* "The default button should be the button that represents the
* action that the user is most likely to perform if that action isn�t
* potentially dangerous."
* <P>The YES_NO options should be approached with special reluctance.
* Microsoft <A HREF="http://msdn.microsoft.com/en-us/library/aa511331.aspx">cautions</A>,
* "Use Yes and No buttons only to respond to yes or no questions."  This seems
* obvious enough, but Apple adds, "Button names should correspond to the action
* the user performs when pressing the button�for example, Erase, Save, or Delete."
* So instead of presenting a YES_NO dialog with the question "Do you want to continue?"
* a better dialog might provide the options "Cancel" and "Continue".  In short: we
* as developers might tend to lazily use this option and phrase dialogs in such
* a way that yes/no options make sense, but in fact the commit buttons should be
* more descriptive.
* <P>Partly because of the need to avoid yes/no questions, <code>DialogFooter</code> introduces the
* dialog type: SAVE_DONT_SAVE_CANCEL_OPTION.  This is mostly straightforward, but
* there is one catch: on Mac the buttons
* are reordered: "Save", "Cancel" and "Don't Save".  This is to conform with standard
* Mac behavior.  (Or, more specifically: because the Apple guidelines
* state that a button that can cause permanent data loss be as physically far
* from a "safe" button as possible.)  On all other platforms the buttons are
* listed in the order "Save", "Don't Save" and "Cancel".
* <P>Also note the field {@link #reverseButtonOrder}
* controls the order each option is presented in the dialog from left-to-right.
* <h3>Platform Differences</h3>
* These are based mostly on studying Apple and Vista interface guidelines.
* <LI> On Mac, command-period acts like the escape key in dialogs.
* <LI> On Mac the Help component is the standard Mac help icon.  On other platforms
* the help component is a {@link com.bric.swing.JLink}.
* <LI> By default button order is reversed on Macs compared to other platforms.  See
* the <code>DialogFooter.reverseButtonOrder</code> field for details.
* <LI> There is a static boolean to control whether button mnemonics should be
* universally activated.  This was added because
* when studying Windows XP there seemed to be no fixed rules for whether to
* use mnemonics or not.  (Some dialogs show them, some dialogs don't.)  So I
* leave it to your discretion to activate them.  I think this boolean should never be
* activated on Vista or Mac, but on XP and Linux flavors: that's up to you.  (Remember
* using the alt key usually activates the mnemonics in most Java look-and-feels, so just
* because they aren't universally active doesn't mean you're hurting accessibility needs.)</LI>
*/
public class DialogFooter extends JPanel
  private static final long serialVersionUID = 1L;
 
  /** This (the default behavior) does nothing when the escape key is pressed. */
  public static final int ESCAPE_KEY_DOES_NOTHING = 0;
 
  /** This triggers the cancel button when the escape key is pressed.  If no
   * cancel button is present: this does nothing.
   * (Also on Macs command+period acts the same as the escape key.)
   * <p>This should only be used if the cancel button does not lead to data
   * loss, because users may quickly press the escape key before reading
   * the text in a dialog.
   */
  public static final int ESCAPE_KEY_TRIGGERS_CANCEL = 1;
 
  /** This triggers the default button when the escape key is pressed.  If no
   * default button is defined: this does nothing.
   * (Also on Macs command+period acts the same as the escape key.)
   * <p>This should only be used if the default button does not lead to data
   * loss, because users may quickly press the escape key before reading
   * the text in a dialog.
   */
  public static final int ESCAPE_KEY_TRIGGERS_DEFAULT = 2;

  /** This triggers the non-default button when the escape key is pressed.  If no
   * non-default button is defined: this does nothing.
   * (Also on Macs command+period acts the same as the escape key.)
   * <p>This should only be used if the non-default button does not lead to data
   * loss, because users may quickly press the escape key before reading
   * the text in a dialog.
   */
  public static final int ESCAPE_KEY_TRIGGERS_NONDEFAULT = 3;
 
  private static KeyStroke escapeKey = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
  private static KeyStroke commandPeriodKey = KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());

  /** The localized strings used in dialogs. */
  public static ResourceBundle strings = ResourceBundle.getBundle("com.bric.swing.resources.DialogFooter");
 
  /** This is the client property of buttons created in static methods by this class. */
  public static String PROPERTY_OPTION = "DialogFooter.propertyOption";
 
  private static int uniqueCtr = 0;
 
  /** Used to indicate the user selected "Cancel" in a dialog.
   * <BR>Also this can be used as a dialog type, to indicate that "Cancel" should
   * be the only option presented to the user.
   * <P>Note the usage is similar to JOptionPane's, but the numerical value is
   * different, so you cannot substitute JOptionPane.CANCEL_OPTION for DialogFooter.CANCEL_OPTION.
   */
  public static final int CANCEL_OPTION = uniqueCtr++;
 
  /** Used to indicate the user selected "OK" in a dialog.
   * <BR>Also this can be used as a dialog type, to indicate that "OK" should
   * be the only option presented to the user.
   * <P>Note the usage is similar to JOptionPane's, but the numerical value is
   * different, so you cannot substitute JOptionPane.OK_OPTION for DialogFooter.OK_OPTION.
   */
  public static final int OK_OPTION = uniqueCtr++;
 
  /** Used to indicate the user selected "No" in a dialog.
   * <BR>Also this can be used as a dialog type, to indicate that "No" should
   * be the only option presented to the user.
   * <P>Note the usage is similar to JOptionPane's, but the numerical value is
   * different, so you cannot substitute JOptionPane.NO_OPTION for DialogFooter.NO_OPTION.
   */
  public static final int NO_OPTION = uniqueCtr++;
 
  /** Used to indicate the user selected "Yes" in a dialog.
   * <BR>Also this can be used as a dialog type, to indicate that "Yes" should
   * be the only option presented to the user.
   * <P>Note the usage is similar to JOptionPane's, but the numerical value is
   * different, so you cannot substitute JOptionPane.YES_OPTION for DialogFooter.YES_OPTION.
   */
  public static final int YES_OPTION = uniqueCtr++;
 
  /** Used to indicate a dialog should present a "Yes" and "No" option.
   * <P>Note the usage is similar to JOptionPane's, but the numerical value is
   * different, so you cannot substitute JOptionPane.YES_NO_OPTION for DialogFooter.YES_NO_OPTION.
   */
  public static final int YES_NO_OPTION = uniqueCtr++;

  /** Used to indicate a dialog should present a "Yes", "No", and "Cancel" option.
   * <P>Note the usage is similar to JOptionPane's, but the numerical value is
   * different, so you cannot substitute JOptionPane.YES_NO_CANCEL_OPTION for DialogFooter.YES_NO_CANCEL_OPTION.
   */
  public static final int YES_NO_CANCEL_OPTION = uniqueCtr++;

  /** Used to indicate a dialog should present a "OK" and "Cancel" option.
   * <P>Note the usage is similar to JOptionPane's, but the numerical value is
   * different, so you cannot substitute JOptionPane.OK_CANCEL_OPTION for DialogFooter.OK_CANCEL_OPTION.
   */
  public static final int OK_CANCEL_OPTION = uniqueCtr++;

  /** Used to indicate a dialog should present a "Save", "Don't Save", and "Cancel" option.
   */
  public static final int SAVE_DONT_SAVE_CANCEL_OPTION = uniqueCtr++;

  /** Used to indicate a dialog should present a "Don't Save" and "Save" option.
   * This will be used for QOptionPaneCommon.FILE_EXTERNAL_CHANGES.
   */
  public static final int DONT_SAVE_SAVE_OPTION = uniqueCtr++;
 
  /** Used to indicate the user selected "Save" in a dialog.
   * <BR>Also this can be used as a dialog type, to indicate that "Save"
   * should be the only option presented to the user.
   */
  public static final int SAVE_OPTION = uniqueCtr++;

  /** Used to indicate the user selected "Don't Save" in a dialog.
   * <BR>Also this can be used as a dialog type, to indicate that "Don't Save"
   * should be the only option presented to the user.
   */
  public static final int DONT_SAVE_OPTION = uniqueCtr++;

  /** Used to indicate the user selected an option not otherwise
   * specified in this set of constants.  It may be possible
   * that the user closed this dialog with the close decoration,
   * or else another agent dismissed this dialog.
   * <p>If you use a safely predesigned set of options this
   * will not be used.
   */
  public static final int UNDEFINED_OPTION = uniqueCtr++;

    private static AncestorListener escapeTriggerListener = new AncestorListener() {

    public void ancestorAdded(AncestorEvent event) {
      JButton button = (JButton)event.getComponent();
      Window w = SwingUtilities.getWindowAncestor(button);
      if(w instanceof RootPaneContainer) {
        setRootPaneContainer(button, (RootPaneContainer)w);
      } else {
        setRootPaneContainer(button, null);
      }
    }
   
    private void setRootPaneContainer(JButton button,RootPaneContainer c) {
      RootPaneContainer lastContainer = (RootPaneContainer)button.getClientProperty("bric.footer.rpc");
      if(lastContainer==c) return;
     
      if(lastContainer!=null) {
        lastContainer.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(escapeKey);
        lastContainer.getRootPane().getActionMap().remove(escapeKey);
       
        if(JVM.isMac)
          lastContainer.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(commandPeriodKey);

      }

      if(c!=null) {
        c.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(escapeKey, escapeKey);
        c.getRootPane().getActionMap().put(escapeKey, new ClickAction(button));
       
        if(JVM.isMac)
          c.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(commandPeriodKey, escapeKey);
      }
      button.putClientProperty("bric.footer.rpc", c);
    }

    public void ancestorMoved(AncestorEvent event) {
      ancestorAdded(event);
    }

    public void ancestorRemoved(AncestorEvent event) {
      ancestorAdded(event);
     
    }
     
    };
 
    /** Creates a new "Cancel" button.
     *
     * @param escapeKeyIsTrigger if true then pressing the escape
     * key will trigger this button.  (Also on Macs command-period will act
     * like the escape key.)  This should be <code>false</code> if this button
     * can lead to permanent data loss.
     */
    public static JButton createCancelButton(boolean escapeKeyIsTrigger) {
      JButton button = new JButton(strings.getString("dialogCancelButton"));
      button.setMnemonic( strings.getString("dialogCancelMnemonic").charAt(0) );
      button.putClientProperty(PROPERTY_OPTION, new Integer(CANCEL_OPTION));
      if(escapeKeyIsTrigger)
        makeEscapeKeyActivate(button);
      return button;
    }
   
    /** This guarantees that when the escape key is pressed
     * (if its parent window has the keyboard focus) this button
     * is clicked.
     * <p>It is assumed that no two buttons will try to consume
     * escape keys in the same window.
     *
     * @param button the button to trigger when the escape key is pressed.
     */
    public static void makeEscapeKeyActivate(AbstractButton button) {
    button.addAncestorListener(escapeTriggerListener);
    }
   
    /** Creates a new "OK" button that is not triggered by the escape key.
     */
    public static JButton createOKButton() {
      return createOKButton(false);
    }

    /** Creates a new "OK" button.
     *
     * @param escapeKeyIsTrigger if true then pressing the escape
     * key will trigger this button.  (Also on Macs command-period will act
     * like the escape key.)  This should be <code>false</code> if this button
     * can lead to permanent data loss.
     */
    public static JButton createOKButton(boolean escapeKeyIsTrigger) {
      JButton button = new JButton(strings.getString("dialogOKButton"));
      button.setMnemonic( strings.getString("dialogOKMnemonic").charAt(0) );
      button.putClientProperty(PROPERTY_OPTION, new Integer(OK_OPTION));
      if(escapeKeyIsTrigger)
        makeEscapeKeyActivate(button);
      return button;
    }
   
    /** Creates a new "Yes" button that is not triggered by the escape key.
     */
    public static JButton createYesButton() {
      return createYesButton(false);
    }
   
    /** Creates a new "Yes" button.
     *
     * @param escapeKeyIsTrigger if true then pressing the escape
     * key will trigger this button.  (Also on Macs command-period will act
     * like the escape key.)  This should be <code>false</code> if this button
     * can lead to permanent data loss.
     */
    public static JButton createYesButton(boolean escapeKeyIsTrigger) {
      JButton button = new JButton(strings.getString("dialogYesButton"));
      button.setMnemonic( strings.getString("dialogYesMnemonic").charAt(0) );
      button.putClientProperty(PROPERTY_OPTION, new Integer(YES_OPTION));
      if(escapeKeyIsTrigger)
        makeEscapeKeyActivate(button);
      return button;
    }

    /** Creates a new "No" button that is not triggered by the escape key.
     *
     */
    public static JButton createNoButton() {
      return createNoButton(false);
    }

    /** Creates a new "No" button.
     *
     * @param escapeKeyIsTrigger if true then pressing the escape
     * key will trigger this button.  (Also on Macs command-period will act
     * like the escape key.)  This should be <code>false</code> if this button
     * can lead to permanent data loss.
     */
    public static JButton createNoButton(boolean escapeKeyIsTrigger) {
      JButton button = new JButton(strings.getString("dialogNoButton"));
      button.setMnemonic( strings.getString("dialogNoMnemonic").charAt(0) );
      button.putClientProperty(PROPERTY_OPTION, new Integer(NO_OPTION));
      if(escapeKeyIsTrigger)
        makeEscapeKeyActivate(button);
      return button;
    }
   
    /** Creates a new "Save" button that is not triggered by the escape key.
     */
    public static JButton createSaveButton() {
      return createSaveButton(false);
    }

    /** Creates a new "Save" button.
     *
     * @param escapeKeyIsTrigger if true then pressing the escape
     * key will trigger this button.  (Also on Macs command-period will act
     * like the escape key.)  This should be <code>false</code> if this button
     * can lead to permanent data loss.
     */
    public static JButton createSaveButton(boolean escapeKeyIsTrigger) {
      JButton button = new JButton(strings.getString("dialogSaveButton"));
      button.setMnemonic( strings.getString("dialogSaveMnemonic").charAt(0) );
      button.putClientProperty(PROPERTY_OPTION, new Integer(SAVE_OPTION));
      if(escapeKeyIsTrigger)
        makeEscapeKeyActivate(button);
      return button;
    }
   
    /** Creates a new "Don't Save" button that is not triggered by the escape key.
     */
    public static JButton createDontSaveButton() {
      return createDontSaveButton(false);
    }

    /** Creates a new "Don't Save" button.
     *
     * @param escapeKeyIsTrigger if true then pressing the escape
     * key will trigger this button.  (Also on Macs command-period will act
     * like the escape key.)  This should be <code>false</code> if this button
     * can lead to permanent data loss.
     */
    public static JButton createDontSaveButton(boolean escapeKeyIsTrigger) {
      String text = strings.getString("dialogDontSaveButton");
      JButton button = new JButton(text);
      button.setMnemonic( strings.getString("dialogDontSaveMnemonic").charAt(0) );
      button.putClientProperty(PROPERTY_OPTION, new Integer(DONT_SAVE_OPTION));
      //Don't know if this documented by Apple, but command-D usually triggers "Don't Save" buttons:
      button.putClientProperty(DialogFooter.PROPERTY_META_SHORTCUT,new Character(text.charAt(0)));
      if(escapeKeyIsTrigger)
        makeEscapeKeyActivate(button);
      return button;
    }
   

    /** Creates a <code>DialogFooter</code> and assigns a default button.
     * The default button is the first button listed in the button type.  For example,
     * a YES_NO_CANCEL_OPTION dialog will make the YES_OPTION the default button.
     *
     * @param options one of the OPTIONS fields in this class, such as YES_NO_OPTION or CANCEL_OPTION.
     * @param escapeKeyBehavior one of the <code>ESCAPE_KEY</code> constants in this class.
     * @return a <code>DialogFooter</code>
     */
    public static DialogFooter createDialogFooter(int options,int escapeKeyBehavior) {
      return createDialogFooter(new JComponent[] {}, options, escapeKeyBehavior);
    }
   
    /** Creates a <code>DialogFooter</code> and assigns a default button.
     * The default button is the first button listed in the button type.  For example,
     * a YES_NO_CANCEL_OPTION dialog will make the YES_OPTION the default button.
     * <P>To use a different default button, use the other <code>createDialogFooter()</code> method.
     *
     * @param leftComponents the components to put on the left side of the footer.
     * <P>The Apple guidelines state that this area is reserved for
     * "button[s] that affect the contents of the dialog itself, such as Reset [or Help]".
     * @param options one of the OPTIONS fields in this class, such as YES_NO_OPTION or CANCEL_OPTION.
     * @param escapeKeyBehavior one of the <code>ESCAPE_KEY</code> constants in this class.
     * @return a <code>DialogFooter</code>
     */
    public static DialogFooter createDialogFooter(JComponent[] leftComponents,int options,int escapeKeyBehavior) {
      if(options==CANCEL_OPTION) {
        return createDialogFooter(leftComponents,options,CANCEL_OPTION,escapeKeyBehavior);
      } else if(options==DONT_SAVE_OPTION) {
        return createDialogFooter(leftComponents,options,DONT_SAVE_OPTION,escapeKeyBehavior);
      } else if(options==NO_OPTION) {
        return createDialogFooter(leftComponents,options,NO_OPTION,escapeKeyBehavior);
      } else if(options==OK_CANCEL_OPTION) {
        return createDialogFooter(leftComponents,options,OK_OPTION,escapeKeyBehavior);
      } else if(options==OK_OPTION) {
        return createDialogFooter(leftComponents,options,OK_OPTION,escapeKeyBehavior);
      } else if(options==SAVE_DONT_SAVE_CANCEL_OPTION) {
        return createDialogFooter(leftComponents,options,SAVE_OPTION,escapeKeyBehavior);
      } else if(options==DONT_SAVE_SAVE_OPTION) {
        return createDialogFooter(leftComponents,options,DONT_SAVE_OPTION,escapeKeyBehavior);
      } else if(options==SAVE_OPTION) {
        return createDialogFooter(leftComponents,options,SAVE_OPTION,escapeKeyBehavior);
      } else if(options==YES_NO_CANCEL_OPTION) {
        return createDialogFooter(leftComponents,options,YES_OPTION,escapeKeyBehavior);
      } else if(options==YES_NO_OPTION) {
        return createDialogFooter(leftComponents,options,YES_OPTION,escapeKeyBehavior);
      } else if(options==YES_OPTION) {
        return createDialogFooter(leftComponents,options,YES_OPTION,escapeKeyBehavior);
      }
      throw new IllegalArgumentException("unrecognized option type ("+options+")");
    }
   

    /** Creates a <code>DialogFooter</code>.
     * @param leftComponents the components to put on the left side of the footer.
     * <P>The Apple guidelines state that this area is reserved for
     * "button[s] that affect the contents of the dialog itself, such as Reset [or Help]".
     * @param options one of the OPTIONS fields in this class, such as YES_NO_OPTION or CANCEL_OPTION.
     * @param defaultButton the OPTION field corresponding to the button that
     * should be the default button, or -1 if there should be no default button.
     * @param escapeKeyBehavior one of the <code>ESCAPE_KEY</code> constants in this class.
     * @return a <code>DialogFooter</code>
     */
    public static DialogFooter createDialogFooter(JComponent[] leftComponents,int options,int defaultButton,int escapeKeyBehavior) {
      JButton[] dismissControls;
      JButton cancelButton = null;
      JButton dontSaveButton = null;
      JButton noButton = null;
      JButton okButton = null;
      JButton saveButton = null;
      JButton yesButton = null;
     
      if(escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_NONDEFAULT) {
        int buttonCount = 1;
        if(options==OK_CANCEL_OPTION || options==YES_NO_OPTION || options==DONT_SAVE_SAVE_OPTION) {
          buttonCount = 2;
        } else if(options==SAVE_DONT_SAVE_CANCEL_OPTION || options==YES_NO_CANCEL_OPTION) {
          buttonCount = 3;
        }
        if(defaultButton!=-1) {
          buttonCount--;
        }
        if(buttonCount>1) {
          throw new IllegalArgumentException("request for escape key to map to "+buttonCount+" buttons.");
        }
      }
     
      if(options==CANCEL_OPTION ||
          options==OK_CANCEL_OPTION ||
          options==SAVE_DONT_SAVE_CANCEL_OPTION ||
          options==YES_NO_CANCEL_OPTION) {
        cancelButton = createCancelButton( escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_CANCEL ||
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_NONDEFAULT && defaultButton!=CANCEL_OPTION) ||
          (defaultButton==CANCEL_OPTION &&
              escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_DEFAULT) );
      }
      if(options==DONT_SAVE_OPTION || options==SAVE_DONT_SAVE_CANCEL_OPTION || options==DONT_SAVE_SAVE_OPTION) {
        dontSaveButton = createDontSaveButton(
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_NONDEFAULT && defaultButton!=DONT_SAVE_OPTION) ||
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_DEFAULT && defaultButton==DONT_SAVE_OPTION ));
      }
      if(options==NO_OPTION || options==YES_NO_OPTION || options==YES_NO_CANCEL_OPTION) {
        noButton = createNoButton(
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_NONDEFAULT && defaultButton!=NO_OPTION) ||
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_DEFAULT && defaultButton==NO_OPTION ));
      }
      if(options==OK_OPTION ||
          options==OK_CANCEL_OPTION) {
        okButton = createOKButton(
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_NONDEFAULT && defaultButton!=OK_OPTION) ||
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_DEFAULT && defaultButton==OK_OPTION ));
      }
      if(options==SAVE_OPTION || options==SAVE_DONT_SAVE_CANCEL_OPTION || options==DONT_SAVE_SAVE_OPTION) {
        saveButton = createSaveButton(
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_NONDEFAULT && defaultButton!=SAVE_OPTION) ||
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_DEFAULT && defaultButton==SAVE_OPTION ));
      }
      if(options==YES_OPTION || options==YES_NO_OPTION || options==YES_NO_CANCEL_OPTION) {
        yesButton = createYesButton(
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_NONDEFAULT && defaultButton!=YES_OPTION) ||
            (escapeKeyBehavior==ESCAPE_KEY_TRIGGERS_DEFAULT && defaultButton==YES_OPTION ));
      }
     
      if(options==CANCEL_OPTION) {
        dismissControls = new JButton[] { cancelButton };
      } else if(options==DONT_SAVE_OPTION) {
        dismissControls = new JButton[] { dontSaveButton };
      } else if(options==NO_OPTION) {
        dismissControls = new JButton[] { noButton };
      } else if(options==OK_CANCEL_OPTION) {
        dismissControls = new JButton[] { okButton, cancelButton };
      } else if(options==OK_OPTION) {
        dismissControls = new JButton[] { okButton };
      } else if(options==DONT_SAVE_SAVE_OPTION) {
        dismissControls = new JButton[] { dontSaveButton, saveButton };
      } else if(options==SAVE_DONT_SAVE_CANCEL_OPTION) {
        setUnsafe(dontSaveButton, true);
        dismissControls = new JButton[] { saveButton, dontSaveButton, cancelButton };
      } else if(options==SAVE_OPTION) {
        dismissControls = new JButton[] { saveButton };
      } else if(options==YES_NO_CANCEL_OPTION) {
        dismissControls = new JButton[] { yesButton, noButton, cancelButton };
      } else if(options==YES_NO_OPTION) {
        dismissControls = new JButton[] { yesButton, noButton };
      } else if(options==YES_OPTION) {
        dismissControls = new JButton[] { yesButton };
      } else {
        throw new IllegalArgumentException("Unrecognized dialog type.");
      }
     

      JButton theDefaultButton = null;
      for(int a = 0; a<dismissControls.length; a++) {
        int i = ((Integer)dismissControls[a].getClientProperty(PROPERTY_OPTION)).intValue();
        if(i==defaultButton)
          theDefaultButton = dismissControls[a];
      }
     
      DialogFooter footer = new DialogFooter(leftComponents,dismissControls,true,theDefaultButton);
      return footer;
    }

  /** This action calls <code>button.doClick()</code>. */
  public static class ClickAction extends AbstractAction {
    private static final long serialVersionUID = 1L;

    JButton button;
   
    public ClickAction(JButton button) {
      this.button = button;
    }
    public void actionPerformed(ActionEvent e) {
      button.doClick();
    }
  }
 
  /** This client property is used to impose a meta-shortcut to click a button.
   * This should map to a Character.
   */
  public static final String PROPERTY_META_SHORTCUT = "Dialog.meta.shortcut";
 
  /** This client property is used to indicate a button is "unsafe".  Apple
   * guidelines state that "unsafe" buttons (such as "discard changes") should
   * be several pixels away from "safe" buttons.
   */
  public static final String PROPERTY_UNSAFE = "Dialog.Unsafe.Action";
 
  /** This indicates whether the dismiss controls should be displayed in reverse
   * order.  When you construct a DialogFooter, the dismiss controls should be listed
   * in order of priority (with the most preferred listed first, the least preferred last).
   * If this boolean is false, then those components will be listed in that order.  If this is
   * true, then those components will be listed in the reverse order.
   * <P>By default on Mac this is true, because Macs put the default button on the right
   * side of dialogs.  On all other platforms this is false by default.
   * <P>Window's <A HREF="http://msdn.microsoft.com/en-us/library/ms997497.aspx">guidelines</A>
   * advise to, "Position the most important button -- typically the default command --
   * as the first button in the set."
   */
  public static boolean reverseButtonOrder = JVM.isMac;
 
  protected JComponent[] leftControls;
  protected JComponent[] dismissControls;
  protected JComponent lastSelectedComponent;
  protected boolean autoClose = false;
  protected JButton defaultButton = null;
  int buttonWidthPadding, buttonHeightPadding, buttonGap, unsafeButtonGap;
  boolean fillWidth = false;
 
  private final ActionListener innerActionListener = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      lastSelectedComponent = (JComponent)e.getSource();
      fireActionListeners(e);

      if(autoClose)
        closeDialogAndDisposeAction.actionPerformed(e);
    }
  };
 
  /** Clones an array of JComponents */
  private static JComponent[] copy(JComponent[] c) {
    JComponent[] newArray = new JComponent[c.length];
    for(int a = 0; a<c.length; a++) {
      newArray[a] = c[a];
    }
    return newArray;
  }

  /** This addresses code that must involve the parent RootPane and Window. */
  private final HierarchyListener hierarchyListener = new HierarchyListener() {
    public void hierarchyChanged(HierarchyEvent e)
    {
      processRootPane();
      processWindow();
    }
    private void processRootPane() {
      JRootPane root = SwingUtilities.getRootPane(DialogFooter.this);
      if(root==null) return;
      root.setDefaultButton(defaultButton);

     
      for(int a = 0; a<dismissControls.length; a++) {
        if(dismissControls[a] instanceof JButton) {
          Character ch = (Character)dismissControls[a].getClientProperty(PROPERTY_META_SHORTCUT);
          if(ch!=null) {
            KeyStroke keyStroke = KeyStroke.getKeyStroke(ch.charValue(), Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
            root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( keyStroke, keyStroke);
            root.getActionMap().put(keyStroke, new ClickAction((JButton)dismissControls[a]));
          }
        }
      }
    }
    private void processWindow() {
      Window window = SwingUtilities.getWindowAncestor(DialogFooter.this);
      if(window==null) return;
     
      window.setFocusTraversalPolicy(new DelegateFocusTraversalPolicy(window.getFocusTraversalPolicy()) {
        private static final long serialVersionUID = 1L;
       
        public Component getDefaultComponent(Container focusCycleRoot) {
          /** If the default component would naturally be in the footer *anyway*:
           * Make sure the default component is the default button.
           *
           * However if the default component lies elsewhere (a text field or
           * check box in the dialog): that should retain the default focus.
           *
           */
          Component defaultComponent = super.getDefaultComponent(focusCycleRoot);
          if(DialogFooter.this.isAncestorOf(defaultComponent)) {
            JButton button = DialogFooter.this.defaultButton;
            if(button!=null && button.isShowing() && button.isEnabled() && button.isFocusable())
              return button;
          }
          return defaultComponent;
        }
      } );
    }
  };
 
  /** Create a new <code>DialogFooter</code>.
   *
   * @param leftControls the controls on the left side of this dialog, such as a help component, or a "Reset" button.
   * @param dismissControls the controls on the right side of this dialog that should dismiss this dialog.  Also
   * called "action" buttons.
   * @param autoClose whether the dismiss buttons should automatically close the containing window.
   * If this is <code>false</code>, then it is assumed someone else is taking care of closing/disposing the
   * containing dialog
   * @param defaultButton the optional button in <code>dismissControls</code> to make the default button in this dialog.
   * (May be null.)
   */
  public DialogFooter(JComponent[] leftControls,JComponent[] dismissControls,boolean autoClose,JButton defaultButton) {
    super(new GridBagLayout());
    this.autoClose = autoClose;
    //this may be common:
    if(leftControls==null) leftControls = new JComponent[] {};
    //erg, this shouldn't be, but let's not throw an error because of it?
    if(dismissControls==null) dismissControls = new JComponent[] {};
    this.leftControls = copy(leftControls);
    this.dismissControls = copy(dismissControls);
    this.defaultButton = defaultButton;
   
    for(int a = 0; a<dismissControls.length; a++) {
      dismissControls[a].putClientProperty("dialog.footer.index", new Integer(a));
      if(dismissControls[a] instanceof JButton) {
        ((JButton)dismissControls[a]).addActionListener(innerActionListener);
      } else {
        //think of things like the JLink: it a label, but it has an ActionListener model
        try {
          Class cl = dismissControls[a].getClass();
          Method m = cl.getMethod("addActionListener", new Class[] {ActionListener.class});
          m.invoke(dismissControls[a], new Object[] {innerActionListener});
        } catch(Throwable t) {
          //do nothing
        }
      }
    }
   
    addHierarchyListener(hierarchyListener);
   
    for(int a = 0; a<leftControls.length; a++) {
      addFocusArrowListener(leftControls[a]);
    }
    for(int a = 0; a<dismissControls.length; a++) {
      addFocusArrowListener(dismissControls[a]);
    }
   
    if(JVM.isMac) {
      setButtonGap(12);
    } else if(JVM.isVista) {
      setButtonGap(8);
    } else {
      setButtonGap(6);
    }
    setUnsafeButtonGap(24);
   
    installGUI();
  }
 
  public void setInternalButtonPadding(int widthPadding,int heightPadding) {
    if(buttonWidthPadding==widthPadding && buttonHeightPadding==heightPadding) {
      return;
    }
    buttonWidthPadding = widthPadding;
    buttonHeightPadding = heightPadding;
    installGUI();
  }
 
  public void setButtonGap(int gap) {
    if(buttonGap==gap)
      return;
    buttonGap = gap;
    installGUI();
  }
 
  public void setFillWidth(boolean b) {
    if(fillWidth==b)
      return;
   
    this.fillWidth = b;
    installGUI();
  }
 
  public void setUnsafeButtonGap(int unsafeGap) {
    if(unsafeButtonGap==unsafeGap)
      return;
    unsafeButtonGap = unsafeGap;
    installGUI();
  }
 
  private void installGUI() {
    removeAll();
   
    GridBagConstraints c = new GridBagConstraints();
    c.gridx = 0; c.gridy = 0;
    c.weightx = 0; c.weighty = 1;
    c.fill = GridBagConstraints.NONE;
    c.insets = new Insets(0,0,0,0);
    c.anchor = GridBagConstraints.CENTER;
    for(int a = 0; a<leftControls.length; a++) {
      add(leftControls[a],c);
      c.gridx++;
      c.insets = new Insets(0,0,0,buttonGap);
    }
    c.weightx = 1;
    c.insets = new Insets(0,0,0,0);
    JPanel fluff = new JPanel();
    fluff.setOpaque(false);
   
    if(leftControls.length>0) {
      add(fluff,c); //fluff to enforce the left and right sides
      c.gridx++;
    }
    c.weightx = 0;
    int unsafeCtr = 0;
    int safeCtr = 0;
    for(int a = 0; a<dismissControls.length; a++) {
      if(JVM.isMac && isUnsafe(dismissControls[a])) {
        unsafeCtr++;
      } else {
        safeCtr++;
      }
    }
    JButton[] unsafeButtons = new JButton[unsafeCtr];
    JButton[] safeButtons = new JButton[safeCtr];
    unsafeCtr = 0;
    safeCtr = 0;
    for(int a = 0; a<dismissControls.length; a++) {
      if(dismissControls[a] instanceof JButton) {
        if(JVM.isMac && isUnsafe(dismissControls[a])) {
          unsafeButtons[unsafeCtr++] = (JButton)dismissControls[a];
        } else {
          safeButtons[safeCtr++] = (JButton)dismissControls[a];
        }
      }
    }
   
    c.ipadx = buttonWidthPadding;
    c.ipady = buttonHeightPadding;
    c.insets = new Insets(0,0,0,0);
    for(int a = 0; a<unsafeButtons.length; a++) {
      JComponent comp = reverseButtonOrder ?
          unsafeButtons[unsafeButtons.length-1-a] :
          unsafeButtons[a];
      add(comp, c);
      c.gridx++;
      c.insets.left = buttonGap;
    }
    if(unsafeButtons.length>0) {
      c.insets.left = unsafeButtonGap;
      if(fillWidth && leftControls.length==0) {
        c.weightx = 1;
        add(fluff, c);
        c.weightx = 0;
        c.gridx++;
      }
    } else if(leftControls.length==0) {
      c.weightx = 1;
      add(fluff, c);
      c.weightx = 0;
      c.gridx++;
    }
   
    for(int a = 0; a<safeButtons.length; a++) {
      JComponent comp = reverseButtonOrder ?
          safeButtons[safeButtons.length-1-a] :
          safeButtons[a];
         
      add(comp, c);
      c.gridx++;
      c.insets.left = buttonGap;
    }
       
    normalizeButtons(unsafeButtons);
    normalizeButtons(safeButtons);
  }

  private static void addFocusArrowListener(JComponent jc) {
    /** Check to see if someone already added this kind of listener:
     */
    KeyListener[] listeners = jc.getKeyListeners();
    for(int a = 0; a<listeners.length; a++) {
      if(listeners[a] instanceof FocusArrowListener)
        return;
    }
    //Add our own:
    jc.addKeyListener(new FocusArrowListener());
  }
 
  /** This takes a set of buttons and gives them all the width/height
   * of the largest button among them.
   * <P>(More specifically, this sets the <code>preferredSize</code>
   * of each button to the largest preferred size in the list of buttons.
   *
   * @param buttons an array of buttons.
   */
  public static void normalizeButtons(JButton[] buttons) {
    int maxWidth = 0;
    int maxHeight = 0;
    for(int a = 0; a<buttons.length; a++) {
      buttons[a].setPreferredSize(null);
      Dimension d = buttons[a].getPreferredSize();
      Number n = (Number)buttons[a].getClientProperty( DialogFooter.PROPERTY_OPTION );
      if( (n!=null && n.intValue()==DialogFooter.DONT_SAVE_OPTION) ||
          d.width>80 ) {
        buttons[a] = null;
      }
      if(buttons[a]!=null) {
        maxWidth = Math.max(d.width, maxWidth);
        maxHeight = Math.max(d.height, maxHeight);
      }
    }
    for(int a = 0; a<buttons.length; a++) {
      if(buttons[a]!=null)
        buttons[a].setPreferredSize(new Dimension(maxWidth,maxHeight));
    }
  }

  /** This indicates that an action button risks losing user's data.
   * On Macs an unsafe button is spaced farther away from safe buttons.
   */
  public static boolean isUnsafe(JComponent c) {
    Boolean b = (Boolean)c.getClientProperty(PROPERTY_UNSAFE);
    if(b==null) b = Boolean.FALSE;
    return b.booleanValue();
  }
 
  /** This sets the unsafe flag for buttons.
   */
  public static void setUnsafe(JComponent c,boolean b) {
    c.putClientProperty(PROPERTY_UNSAFE, new Boolean(b));
  }
 
  private Vector listeners;
 
  /** Adds an <code>ActionListener</code>.
   *
   * @param l this listener will be notified when a <code>dismissControl</code> is activated.
   */
  public void addActionListener(ActionListener l) {
    if(listeners==null) listeners = new Vector();
    if(listeners.contains(l))
      return;
    listeners.add(l);
  }
 
  /** Removes an <code>ActionListener</code>.
   */
  public void removeActionListener(ActionListener l) {
    if(listeners==null) return;
    listeners.remove(l);
  }
 
  private void fireActionListeners(ActionEvent e) {
    if(listeners==null) return;
    for(int a = 0; a<listeners.size(); a++) {
      ActionListener l = (ActionListener)listeners.get(a);
      try {
        l.actionPerformed(e);
      } catch(Exception e2) {
        e2.printStackTrace();
      }
    }
  }
 
  /** Returns the component last used to dismiss the dialog.
   * <P>Note the components on the left side of this footer
   * (such as a "Help" button or a "Reset Preferences" button)
   * do NOT dismiss the dialog, and so this method has nothing
   * to do with those components.  This only relates to the components on the
   * right side of dialog.
   * @return the component last used to dismiss the dialog.
   */
  public JComponent getLastSelectedComponent() {
    return lastSelectedComponent;
  }
 
  /** Finds a certain type of button, if it is available.
   *
   * @param buttonType of the options in this class (such as YES_OPTION or CANCEL_OPTION)
   * @return the button that maps to that option, or null if no such button was found.
   */
  public JButton getButton(int buttonType) {
    for(int a = 0; a<getComponentCount(); a++) {
      if(getComponent(a) instanceof JButton) {
        JButton button = (JButton)getComponent(a);
        Object value = button.getClientProperty(PROPERTY_OPTION);
        int intValue = -1;
        if(value instanceof Number)
          intValue = ((Number)value).intValue();
        if(intValue==buttonType)
          return button;
      }
    }
    return null;
  }

  /** Returns true if a certain button type is available in this footer.
   *
   * @param buttonType of the options in this class (such as YES_OPTION or CANCEL_OPTION)
   * @return true if a button corresponding to the option provided exists.
   */
  public boolean containsButton(int buttonType) {
    return getButton(buttonType)!=null;
  }
 
  /** This resets the value of <code>lastSelectedComponent</code> to null.
   * <P>If this footer is recycled in different dialogs, then you may
   * need to nullify this value for <code>getLastSelectedComponent()</code>
   * to remain relevant.
   */
  public void reset() {
    lastSelectedComponent = null;
  }
 
  /** Returns a copy of the <code>dismissControls</code> array used to construct this footer. */
  public JComponent[] getDismissControls() {
    return copy(dismissControls);
  }
 
  public void setAutoClose(boolean b) {
    autoClose = b;
  }

  /** Returns a copy of the <code>leftControls</code> array used to construct this footer. */
  public JComponent[] getLeftControls() {
    return copy(leftControls);
  }
 
  /** This action takes the Window associated with the source of this event,
   * hides it, and then calls <code>dispose()</code> on it.
   * <P>(This will not throw an exception if there is no parent window,
   * but it does nothing in that case...)
   */
  public static Action closeDialogAndDisposeAction = new AbstractAction() {
    private static final long serialVersionUID = 1L;

    public void actionPerformed(ActionEvent e) {
      Component src = (Component)e.getSource();
      Container parent = src.getParent();
      while(parent!=null) {
        if( parent instanceof JInternalFrame ) {
          ((JInternalFrame)parent).setVisible(false);
          ((JInternalFrame)parent).dispose();
          return;
        } else if(parent instanceof Window) {
          ((Window)parent).setVisible(false);
          ((Window)parent).dispose();
          return;
        }
        parent = parent.getParent();
      }
    }
  };
}
TOP

Related Classes of com.bric.swing.DialogFooter$ClickAction

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.